/**
 * License would add here
 */

#include "log.h"
#include <cstdint>
#ifndef TERM_WHEN_FAILED
#define TERM_WHEN_FAILED 0
#endif

#ifndef OHOS_PLATFORM
#include <node_api.h>
#else
#include <napi/native_api.h>
#endif

#include "js_function.h"
#include "js_number.h"
#include "js_value.h"
#include "js_value_ref.h"

thread_local JSValueRef *JSNumber::g_numberIsFinite = nullptr;
thread_local JSValueRef *JSNumber::g_numberIsInteger = nullptr;
thread_local JSValueRef *JSNumber::g_numberIsNaN = nullptr;
thread_local JSValueRef *JSNumber::g_numberIsSafeInteger = nullptr;

struct JsUtilsCleanupData
{
    size_t count_{0};
    JSValueRef **refs_{nullptr};
};

static void JsUtilsCleanup(void *data)
{
#define LOCAL_LOG_TAG "CLEANUP_HOOK"
    JsUtilsCleanupData *that = reinterpret_cast<JsUtilsCleanupData *>(data);
    JSValueRef **refs = that->refs_;
    if (refs == nullptr)
    {
        return;
    }
    for (size_t i = 0; i < that->count_; i++)
    {
        JSValueRef *ref = refs[i];
        if (ref != nullptr)
        {
            delete ref;
        }
        refs[i] = nullptr;
    }
    Logger(LogLevel::LOG_DEBUG, LOCAL_LOG_TAG) << "js utils is freed, count: " << that->count_;
    delete refs;
#undef LOCAL_LOG_TAG
}

#define JS_UTIL_REFS_BLOCK_SIZE 4

JSValueRef **AllocateUtilsBlock(JSValueRef **buffer, size_t *allocated, size_t index)
{
#define LOCAL_LOG_TAG "JS_UTILS"
    if (allocated == nullptr)
    {
        Logger(LogLevel::LOG_FATAL, LOCAL_LOG_TAG) << "allocated size cannot be nullptr";
    }
    if (buffer == nullptr && *allocated > 0)
    {
        Logger(LogLevel::LOG_FATAL, LOCAL_LOG_TAG) << "allocated buffer cannot be empty";
    }
    if (index >= *allocated)
    {
        *allocated += JS_UTIL_REFS_BLOCK_SIZE;
        Logger(LogLevel::LOG_DEBUG, LOCAL_LOG_TAG) << "buffer is reallocted, size: " << *allocated;
        JSValueRef **reallocated = new JSValueRef *[*allocated];
        for (size_t i = 0; i < *allocated - JS_UTIL_REFS_BLOCK_SIZE; i++)
        {
            reallocated[i] = buffer[i];
        }
        if (buffer != nullptr)
        {
            delete[] buffer;
            buffer = nullptr;
        }
        return reallocated;
    }
    return buffer;
#undef LOCAL_LOG_TAG
}

#define INIT_JS_UTILS(refs, jscons, field, method)                                       \
    if (field == nullptr)                                                                \
    {                                                                                    \
        Logger(LogLevel::LOG_DEBUG) << "js method \"" #method "\" is inited as " #field; \
        field = new JSValueRef(jscons[#method]);                                         \
        refs = AllocateUtilsBlock(refs, &preAllocated, inited + 1);                      \
        refs[inited] = field;                                                            \
        inited++;                                                                        \
    }

void JSNumber::InitNumberUtils(napi_env env)
{
    if (env == nullptr)
    {
        return;
    }

    if (g_numberIsFinite != nullptr && g_numberIsInteger != nullptr && g_numberIsNaN != nullptr && g_numberIsSafeInteger != nullptr)
    {
        return;
    }

    size_t preAllocated = 0;
    JSValueRef **utilsRef = nullptr;

    size_t inited = 0;
    JSObject global(GetGlobal(env));
    JSObject constructor(global["Number"]);

    INIT_JS_UTILS(utilsRef, constructor, g_numberIsFinite, isFinite)
    INIT_JS_UTILS(utilsRef, constructor, g_numberIsInteger, isInteger)
    INIT_JS_UTILS(utilsRef, constructor, g_numberIsNaN, isNaN)
    INIT_JS_UTILS(utilsRef, constructor, g_numberIsSafeInteger, isSafeInteger)

    JsUtilsCleanupData *data = new JsUtilsCleanupData;
    data->count_ = inited;
    data->refs_ = utilsRef;
    napi_add_env_cleanup_hook(env, JsUtilsCleanup, data);
}

#define NUMBER_JS_UTILS(method)                             \
    JSBoolean JSNumber::method()                            \
    {                                                       \
        InitNumberUtils(env_);                              \
        return JSFunction(*g_number##method).Call({*this}); \
    }
NUMBER_JS_UTILS(IsFinite)
NUMBER_JS_UTILS(IsInteger)
NUMBER_JS_UTILS(IsNaN)
NUMBER_JS_UTILS(IsSafeInteger)

thread_local JSValueRef *JSFunction::g_functionBind = nullptr;
void JSFunction::InitFunctionUtils(napi_env env)
{
    if (env == nullptr)
    {
        return;
    }
    if (g_functionBind != nullptr)
    {
        return;
    }
    size_t preAllocated = 0;
    JSValueRef **utilsRef = nullptr;

    size_t inited = 0;
    JSObject global(GetGlobal(env));
    JSObject constructor(global["Function"]);

    INIT_JS_UTILS(utilsRef, constructor, g_functionBind, bind)

    JsUtilsCleanupData *data = new JsUtilsCleanupData;
    data->count_ = inited;
    data->refs_ = utilsRef;
    napi_add_env_cleanup_hook(env, JsUtilsCleanup, data);
}

JSFunction JSFunction::MakeBound() const
{
    InitFunctionUtils(env_);
    JSFunction makeBound(*g_functionBind);
    napi_value *args = new napi_value[boundArgc_ + 1];
    args[0] = thisVar_;
    for (size_t i = 0; i < boundArgc_; i++)
    {
        args[i + 1] = boundArgv_[i];
    }
    JSFunction boundFunc(makeBound.Apply(value_, boundArgc_ + 1, args));
    delete[] args;
    return boundFunc;
}

JSValueRef::JSValueRef(const JSFunction &value)
    : env_(value.env_)
{
    JSFunction func(value.MakeBound());
    napi_create_reference(env_, func, 1, &ref_);
}

template <>
JSObject::PropertyLValue<JSObject, JSValue>::PropertyLValue(const JSObject &object, const JSValue &key)
    : JSValue(object.GetEnv())
    , env_(object.GetEnv())
    , object_(object)
    , key_(key)
{
    napi_get_property(env_, object, key, &value_);
    UpdateType();
}

template <>
JSObject::PropertyLValue<JSArray, uint32_t>::PropertyLValue(const JSArray &object, const uint32_t &key)
    : JSValue(object.GetEnv())
    , env_(object.GetEnv())
    , object_(object)
    , key_(key)
{
    napi_get_element(env_, object, key, &value_);
    UpdateType();
}

template <>
JSObject::PropertyLValue<JSObject, JSValue> &JSObject::PropertyLValue<JSObject, JSValue>::operator=(const JSValue &value)
{
    napi_set_property(env_, object_, key_, value);
    value_ = value;
    UpdateType();
    return *this;
};

template <>
JSObject::PropertyLValue<JSArray, uint32_t> &JSObject::PropertyLValue<JSArray, uint32_t>::operator=(const JSValue &value)
{
    napi_set_element(env_, object_, key_, value);
    value_ = value;
    UpdateType();
    return *this;
};

const JSObject::PropertyLValue<JSObject, JSValue> JSObject::Get(const JSValue &key) const
{
    return JSObject::PropertyLValue<JSObject, JSValue>(*this, key);
}
const JSObject::PropertyLValue<JSObject, JSValue> JSObject::Get(const napi_value &key) const
{
    return Get(JSValue(env_, key));
}
const JSObject::PropertyLValue<JSObject, JSValue> JSObject::Get(const std::string &key) const
{
    return Get(JSString(env_, key));
}
const JSObject::PropertyLValue<JSObject, JSValue> JSObject::Get(const char *key) const
{
    return Get(JSString(env_, key));
}

const JSObject::PropertyLValue<JSArray, uint32_t> JSArray::Get(uint32_t index) const
{
    return JSObject::PropertyLValue<JSArray, uint32_t>(*this, index);
}